﻿// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// Date: April 25 2010 

#region

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Web;
using System.Web.SessionState;

#endregion

namespace Salient.Web.UserFiles
{
    ///<summary>
    ///</summary>
    public class UserFilesHandler : IHttpAsyncHandler, IRequiresSessionState
    {
        private AsyncProcessorDelegate _delegate;

        #region Static Properties

        private static string _dataDirectory = "~/sessionFiles";
        private static string _dataDirectoryKey = "__userFiles";
        private static string _sessionItemKey = "__userFiles";


        ///<summary>
        ///</summary>
        public static string DataDirectory
        {
            get { return _dataDirectory; }
            set { _dataDirectory = value; }
        }

        ///<summary>
        ///</summary>
        public static string DataDirectoryKey
        {
            get { return _dataDirectoryKey; }
            set { _dataDirectoryKey = value; }
        }

        ///<summary>
        ///</summary>
        public static string SessionItemKey
        {
            get { return _sessionItemKey; }
            set { _sessionItemKey = value; }
        }

        #endregion

        #region IHttpAsyncHandler Members

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests. 
        ///                 </param>
        public void ProcessRequest(HttpContext context)
        {
            context.Response.ClearHeaders();
            context.Response.ClearContent();

            try
            {
                string dbFilePath =
                    Path.Combine((string) context.Application[DataDirectoryKey], context.Session.SessionID + ".db");

                SQLiteLoader loader = new SQLiteLoader(dbFilePath);

                string path = context.Request.Url.PathAndQuery;
                if (context.Request.Url.Query.Length > 0)
                {
                    path = path.Substring(0, path.Length - context.Request.Url.Query.Length);
                }

                string requestType = path.Substring(path.LastIndexOf("/", StringComparison.OrdinalIgnoreCase) + 1);

                switch (requestType.ToUpperInvariant())
                {
                    case "GETENTRYINFO":
                        GetEntryInfo(context, loader);
                        break;
                    case "GETFILE":
                        GetFile(context, loader);
                        break;
                    case "CREATEDIR":
                        CreateDir(context, loader);
                        break;
                    case "PUTFILE":
                        PutFile(context, loader);
                        break;
                    case "LISTDIR":
                        ListDirectory(context, loader);
                        break;
                    case "DELETE":
                        Delete(context, loader);
                        break;
                    case "CLEAR":
                        Clear(loader);
                        break;
                    default:
                        throw new InvalidOperationException(string.Format("Unknown Operation\r\nName: {0}", requestType));
                }
            }
            catch (Exception ex)
            {
                context.WriteJsonResponse(new JsonException
                    {Type = ex.GetType().FullName, Message = ex.Message, StackTrace = ex.StackTrace});
            }
        }

        /// <summary>
        /// Initiates an asynchronous call to the HTTP handler.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.IAsyncResult"/> that contains information about the status of the process.
        /// </returns>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests. 
        ///                 </param><param name="cb">The <see cref="T:System.AsyncCallback"/> to call when the asynchronous method call is complete. If <paramref name="cb"/> is null, the delegate is not called. 
        ///                 </param><param name="extraData">Any extra data needed to process the request. 
        ///                 </param>
        public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
        {
            _delegate = new AsyncProcessorDelegate(ProcessRequest);
            return _delegate.BeginInvoke(context, cb, extraData);
        }

        /// <summary>
        /// Provides an asynchronous process End method when the process ends.
        /// </summary>
        /// <param name="result">An <see cref="T:System.IAsyncResult"/> that contains information about the status of the process. 
        ///                 </param>
        public void EndProcessRequest(IAsyncResult result)
        {
            _delegate.EndInvoke(result);
        }

        /// <summary>
        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
        /// </summary>
        /// <returns>
        /// true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.
        /// </returns>
        public bool IsReusable
        {
            get { return false; }
        }

        #endregion

        /// <summary>
        /// Resets the data file
        /// </summary>
        /// <param name="loader"></param>
        private static void Clear(SQLiteLoader loader)
        {
            loader.Clear();
        }

        /// <summary>
        /// Expects one HttpPostedFile and an optional 'directory' parameter
        /// </summary>
        /// <param name="context"></param>
        /// <param name="loader"></param>
        /// <exception cref="ArgumentNullException">If file data is null or empty</exception>
        /// TODO: implement quota
        private static void PutFile(HttpContext context, SQLiteLoader loader)
        {
            context.SetCachingNever();

            if (context.Request.Files.Count == 0)
            {
                throw new ArgumentNullException("f" + "iles");
            }

            List<EntryInfo> entries = new List<EntryInfo>();

            //foreach (string key in context.Request.Files.AllKeys)
            {
                HttpPostedFile file = context.Request.Files[0];

                string directory = context.Request["directory"];

                if (string.IsNullOrEmpty(directory))
                {
                    directory = "/";
                }

                string entryPath = String.Format(CultureInfo.InvariantCulture, "{0}/{1}", Helpers.FixPath(directory),
                                                 Path.GetFileName(file.FileName));

                // no option but to fully read the file into memory. If we were streaming directly to disk or into SQL Server we
                // could chunk it but as it is there is no other option.
                byte[] data = Helpers.ReadFully(file.InputStream, (int) file.InputStream.Length);

                EntryInfo entry = loader.SaveEntry(entryPath, data);
                entry.Data = null;
                entries.Add(entry);
            }


            context.WriteJsonResponse(entries);
        }

        /// <summary>
        /// Expects a Guid parseable string request parameter 'id'.
        /// Nested directories and files will be deleted recursively.
        /// </summary>
        /// <param name="context"></param>
        /// <param name="loader"></param>
        /// <exception cref="ArgumentException">If request param 'id' is not a valid guid</exception>
        private static void Delete(HttpContext context, SQLiteLoader loader)
        {
            context.SetCachingNever();

            Guid id;
            if (!Helpers.GuidTryParse(context.Request["id"], out id))
            {
                throw new ArgumentException("id");
            }

            loader.DeleteEntry(id);
        }


        /// <summary>
        /// Expects a Guid parseable string request parameter 'id' identifying a Directory entry.
        /// If 'id' is null, the root,'/', will be listed.
        /// Optional request parameters: 
        ///    'recurse' bool : true  - requested directory recursively populated with all nested subdirectories and files suitable for servicing a dual pane directory/file view.
        ///    'flat'    bool : false - flattens response into an array of entries suitable for servicing a consolidated flat table view.
        ///    'files'   bool : true  - include files in listing
        ///    'dirs'    bool : true  - include directories in listing
        /// 
        /// If flat, a JSON string representing a flat array of all entries is rendered
        /// If not flat, a JSON string representing a Single Entry is rendered
        /// </summary>
        /// <param name="context"></param>
        /// <param name="loader"></param>
        /// <exception cref="FileNotFoundException">If directory does not exist</exception>
        /// <exception cref="ArgumentException">If id is malformed</exception>
        private static void ListDirectory(HttpContext context, SQLiteLoader loader)
        {
            context.SetCachingNever();
            Guid id;
            string guid = context.Request["id"];
            if (string.IsNullOrEmpty(guid))
            {
                id = Guid.Empty;
            }
            else if (!Helpers.GuidTryParse(guid, out id))
            {
                throw new ArgumentException();
            }

            string filesParam = context.Request["files"];
            bool files = string.IsNullOrEmpty(filesParam) || bool.Parse(filesParam);
            string dirsParam = context.Request["dirs"];
            bool dirs = string.IsNullOrEmpty(dirsParam) || bool.Parse(dirsParam);
            string recurseParam = context.Request["recurse"];
            bool recurse = string.IsNullOrEmpty(recurseParam) || bool.Parse(recurseParam);
            string flatParam = context.Request["flat"];
            bool flat = !string.IsNullOrEmpty(flatParam) && bool.Parse(flatParam);

            EntryInfo result = loader.GetEntry(id, false, files, dirs, recurse);

            if (flat)
            {
                List<EntryInfo> flatList = new List<EntryInfo>();
                SQLiteLoader.FlattenEntry(result, flatList);
                flatList.Sort((a, b) => string.Compare(a.Path, b.Path, StringComparison.OrdinalIgnoreCase));
                context.WriteJsonResponse(flatList);
            }
            else
            {
                context.WriteJsonResponse(result);
            }
        }

        /// <summary>
        /// Expects request parameter 'directory'. 
        /// Path segments will be created as needed.
        /// Resulting directory entry is rendered
        /// 
        /// Note: the consumer should restrict values to valid directory identifiers
        /// </summary>
        /// <param name="context"></param>
        /// <param name="loader"></param>
        /// <exception cref="ArgumentNullException">If request param 'directory' is null or empty</exception>
        private static void CreateDir(HttpContext context, SQLiteLoader loader)
        {
            context.SetCachingNever();
            string uploadDirectory = context.Request["directory"];
            if (string.IsNullOrEmpty(uploadDirectory))
            {
                throw new ArgumentNullException("directo" + "ry");
            }

            EntryInfo entry = loader.EnsureDirectory(uploadDirectory);
            context.WriteJsonResponse(entry);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="loader"></param>
        /// <exception cref="ArgumentException">If request param 'id' malformed</exception>
        /// <exception cref="FileNotFoundException">If requested file does not exist</exception>
        /// <exception cref="ArgumentNullException">If neither 'path' nor 'id' are supplied</exception>
        private static void GetEntryInfo(HttpContext context, SQLiteLoader loader)
        {
            EntryInfo entry = null;

            if (!string.IsNullOrEmpty(context.Request["id"]))
            {
                Guid id;
                if (!Helpers.GuidTryParse(context.Request["id"], out id))
                {
                    throw new ArgumentException("id");
                }

                entry = loader.GetEntry(id, true);
            }
            else if (!string.IsNullOrEmpty(context.Request["path"]))
            {
                string path = context.Request["path"];
                if (!string.IsNullOrEmpty(path))
                {
                    entry = loader.GetEntry(path, false);
                }
            }
            else
            {
                throw new ArgumentNullException("id or path must be suppl" + "ied");
            }

            if (entry == null)
            {
                throw new FileNotFoundException();
            }

            context.WriteJsonResponse(entry);
        }

        /// <summary>
        /// Expects a Guid parseable string request parameter 'id' identifying a File entry.
        /// File data will be streamed inline with original filename and appropriate
        /// content-type.
        /// 
        /// No exceptions are thrown as we don't expect the output of this method
        /// to be computed.
        /// 
        /// HttpStatus codes represent error conditions
        /// </summary>
        /// <param name="context"></param>
        /// <param name="loader"></param>
        private static void GetFile(HttpContext context, SQLiteLoader loader)
        {
            //TODO: implement smart if-modified
            context.SetCachingNever();
            try
            {
                Guid id;
                string guid = context.Request["id"];
                if (string.IsNullOrEmpty(guid))
                {
                    throw new ArgumentNullException("i" + "d");
                }
                if (!Helpers.GuidTryParse(guid, out id))
                {
                    throw new ArgumentException("id");
                }

                EntryInfo entry = loader.GetEntry(id, true);

                if (entry == null)
                {
                    throw new FileNotFoundException();
                }

                context.Response.AddHeader("Content-Disposition", "inline; filename=" + Path.GetFileName(entry.Path));
                context.Response.AddHeader("Content-Length", entry.Data.Length.ToString(CultureInfo.InvariantCulture));
                context.Response.ContentType = Helpers.GetContentType(entry.Path);

                context.Response.OutputStream.Write(entry.Data, 0, entry.Data.Length);
            }
            catch (FileNotFoundException)
            {
                context.Response.StatusCode = (int) HttpStatusCode.NotFound;
            }
            catch
            {
                context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
            }
            context.Response.Flush();
            context.Response.End();
        }

        #region Nested type: AsyncProcessorDelegate

        private delegate void AsyncProcessorDelegate(HttpContext context);

        #endregion
    }
}